/*
This file is part of [ahttp] library. 

Author: Artem Kustikov (kustikoff[at]tut.by)


This code is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this code.

Permission is granted to anyone to use this code for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:

1. The origin of this code must not be misrepresented; you must
not claim that you wrote the original code. If you use this
code in a product, an acknowledgment in the product documentation
would be appreciated but is not required.

2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original code.

3. This notice may not be removed or altered from any source
distribution.
*/


#include "aconnect/config.hpp"
#include "aconnect/boost_format_safe.hpp"

#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>

#include <assert.h>

#include "aconnect/util.hpp"

#include "ahttp/http_support.hpp"
#include "ahttp/http_server_settings.hpp"

#if defined(__GNUC__)
#	include <dlfcn.h>
#endif

namespace algo = boost::algorithm;
namespace fs = boost::filesystem;

#include "http_messages.inl"

namespace ahttp
{
	DirectorySettings::DirectorySettings () : 
		number (0),
		browsingEnabled (Tristate::Undefined), 
		isLinkedDirectory(false),
		maxRequestSize (-1),
		enableParentPathAccess (Tristate::Undefined),
		parentServerSettings (NULL)
	{

	}

	void DirectorySettings::processTemplates()
	{
		// add tabulators and EOLs
		algo::replace_all (headerTemplate, ahttp::SettingsConstants::TabulatorMark, "\t");
		algo::replace_all (parentDirectoryTemplate, ahttp::SettingsConstants::TabulatorMark, "\t");
		algo::replace_all (fileTemplate, ahttp::SettingsConstants::TabulatorMark, "\t");
		algo::replace_all (directoryTemplate, ahttp::SettingsConstants::TabulatorMark, "\t");
		algo::replace_all (virtualDirectoryTemplate, ahttp::SettingsConstants::TabulatorMark, "\t");
		algo::replace_all (footerTemplate, ahttp::SettingsConstants::TabulatorMark, "\t");

		algo::replace_all (headerTemplate, ahttp::SettingsConstants::EndOfLineMark, "\r\n");
		algo::replace_all (parentDirectoryTemplate, ahttp::SettingsConstants::EndOfLineMark, "\r\n");
		algo::replace_all (fileTemplate, ahttp::SettingsConstants::EndOfLineMark, "\r\n");
		algo::replace_all (directoryTemplate, ahttp::SettingsConstants::EndOfLineMark, "\r\n");
		algo::replace_all (virtualDirectoryTemplate, ahttp::SettingsConstants::EndOfLineMark, "\r\n");
		algo::replace_all (footerTemplate, ahttp::SettingsConstants::EndOfLineMark, "\r\n");
	}

	HttpServerSettings::HttpServerSettings() :
		InstanceId (1),
		IsKeepAliveEnabled (settings_defaults::EnableKeepAlive),
		KeepAliveTimeout (settings_defaults::KeepAliveTimeout),
		IsGzipDeflateEnabled (settings_defaults::EnableGzipDeflate),
		CommandSocketTimeout (settings_defaults::CommandSocketTimeout),
		UploadCreationTriesCount (settings_defaults::UploadCreationTriesCount),
		ResponseBufferSize (settings_defaults::ResponseBufferSize),
		MaxChunkSize (settings_defaults::MaxChunkSize),

		_port(-1), 
		_commandPort (-1),
		_logger (NULL),
		_serverVersion (settings_defaults::ServerVersion)
		
	{
		ConnectionSettings.socketReadTimeout = settings_defaults::ServerSocketTimeout;
		ConnectionSettings.socketWriteTimeout = settings_defaults::ServerSocketTimeout;

		initMessages ();
	}

	HttpServerSettings::~HttpServerSettings()
	{
	}

	string HttpServerSettings::getMimeType (string_constref ext) const 
	{
		using namespace aconnect;
		
		str2str_map::const_iterator it = _mimeTypes.find(ext);
		if (it != _mimeTypes.end())
			return it->second;

		return strings::ContentTypeOctetStream;			
	}

	void HttpServerSettings::loadMimeType (string_constref ext, string_constptr typeDesc)
	{
		if (typeDesc)
			_mimeTypes [ext] = typeDesc;
	}

	const DirectorySettings* HttpServerSettings::getDirSettingsByName(string_constref dirName) const	
	{
		directories_map::const_iterator it = _directories.begin();

		for (;it != _directories.end(); ++it) {
			if (aconnect::util::equals(it->second.name, dirName)) 
				return &it->second;
		}
		
		return 0;			
	}

	const DirectorySettings* HttpServerSettings::getDirSettings(int number) const	
	{
		directories_map::const_iterator it = _directories.begin();

		for (;it != _directories.end(); ++it) {
			if (it->second.number == number)
				return &it->second;
		}
		
		return 0;			
	}

	void HttpServerSettings::setupRootDirectory (std::vector <DirectorySettings>& directoriesList, 
			boost::function1<void, std::vector<DirectorySettings>::iterator > locaInfoLoadCallback) throw (settings_load_error)
	{
		
		std::vector <DirectorySettings>::iterator it = directoriesList.end();
		for (it = directoriesList.begin(); 
			it != directoriesList.end() && !util::equals(it->name, _rootDirName); ++it );

		if (it == directoriesList.end())
			throw settings_load_error ("There is no directory record with name \"%s\"", 
				_rootDirName.c_str());

		if (it->realPath.empty())
			throw settings_load_error ("Empty path in root directory record");

		// work with filesystem
		try
		{
			fs::path rootPath (it->realPath, fs::portable_directory_name);

			if ( !fs::exists (rootPath) )
				throw settings_load_error ("Incorrect path in root directory record - path does not exist");
			
			if ( !fs::is_directory (rootPath) )
				throw settings_load_error ("Incorrect path in root directory record - target is not a directory");

			it->realPath = rootPath.directory_string();
			it->virtualPath = strings::Slash;

			// try to load local directory configuration
			if (locaInfoLoadCallback != NULL)
				locaInfoLoadCallback (it);
			
			// setup defaults
			if (it->maxRequestSize == (size_t) -1) // is not loaded
				it->maxRequestSize = settings_defaults::MaxRequestSize;

			// register root
			it->number = 0;
			loadGlobalModules (*it);
			
			// apply default templates
			if (it->headerTemplate.empty()) it->headerTemplate = settings_defaults::HeaderTemplate;
			if (it->parentDirectoryTemplate.empty()) it->parentDirectoryTemplate = settings_defaults::ParentDirectoryTemplate;
			if (it->directoryTemplate.empty()) it->directoryTemplate = settings_defaults::DirectoryTemplate;
			if (it->virtualDirectoryTemplate.empty()) it->virtualDirectoryTemplate = settings_defaults::VirtualDirectoryTemplate;
			if (it->fileTemplate.empty()) it->fileTemplate = settings_defaults::FileTemplate;
			if (it->footerTemplate.empty()) it->footerTemplate = settings_defaults::FooterTemplate;	
			it->processTemplates();
		
			_directories[it->virtualPath] = *it;

			fillDirectoriesMap (directoriesList, it, locaInfoLoadCallback);

			fillModulesCallbackInfo();

		} catch (fs::basic_filesystem_error<fs::path> &err) {
			throw settings_load_error (
				"Directories info loading failed - 'basic_filesystem_error' caught: %s, "
				"path 1: %s, path 2: %s", 
				err.what(), 
				err.path1().string().c_str(),
				err.path2().string().c_str()); 
		} catch (std::exception &ex)  {
			throw settings_load_error ("Directories info loading failed - exception [%s]: %s", 
				typeid(ex).name(), ex.what());;

		} catch (...)  {
			throw settings_load_error ("Directories info loading failed - unknown exception caught");
		}
	}

	
	void HttpServerSettings::fillDirectoriesMap (std::vector<ahttp::DirectorySettings>& directoriesList, 
												 std::vector<ahttp::DirectorySettings>::iterator parent,
												 boost::function1<void, std::vector<ahttp::DirectorySettings>::iterator > locaInfoLoadCallback)
	{
		using namespace aconnect;
		
		std::vector <DirectorySettings>::iterator childIter;
		for (childIter = directoriesList.begin(); childIter != directoriesList.end(); ++childIter ) 
		{
			if (childIter->parentName == parent->name) 
			{
				if (childIter->virtualPath.empty())
					throw settings_load_error ("Empty <virtual-path> for nested directory: %s", 
					childIter->name.c_str());
				
				string virtualPathInit = childIter->virtualPath;

				childIter->virtualPath = parent->virtualPath + childIter->virtualPath;
				if (childIter->virtualPath.substr(childIter->virtualPath.length() - 1) != strings::Slash)
					childIter->virtualPath += strings::Slash;

				// get real path
				fs::path childPath;
				if (!childIter->realPath.empty()) {
					childPath = fs::path (childIter->realPath, fs::portable_directory_name);
					childIter->isLinkedDirectory = true;
					childIter->relativePath = virtualPathInit;
				
				} else {
					childPath = fs::complete (fs::path (childIter->relativePath, fs::portable_directory_name),
						fs::path (parent->realPath, fs::native));
				}

				if ( !fs::exists (childPath) )
					throw settings_load_error ("Incorrect path in <directory> record - path does not exist,\
											   directory: %s", childIter->name.c_str());
				if ( !fs::is_directory (childPath) )
					throw settings_load_error ("Incorrect path in <directory> record - target is not a directory,\
											   directory: %s", childIter->name.c_str());

				childIter->realPath = childPath.directory_string();


				// try to load local directory configuration
				if (locaInfoLoadCallback != NULL)
					locaInfoLoadCallback(childIter);
				
				// copy properties
				if (childIter->browsingEnabled == Tristate::Undefined) childIter->browsingEnabled = parent->browsingEnabled;
				if (childIter->charset.empty()) childIter->charset = parent->charset;
				if (childIter->maxRequestSize == (size_t) -1) childIter->maxRequestSize = parent->maxRequestSize;
				if (childIter->enableParentPathAccess == Tristate::Undefined) childIter->enableParentPathAccess = parent->enableParentPathAccess;

				if (childIter->headerTemplate.empty())	childIter->headerTemplate = parent->headerTemplate;
				if (childIter->parentDirectoryTemplate.empty())	childIter->parentDirectoryTemplate = parent->parentDirectoryTemplate;
				if (childIter->fileTemplate.empty())	childIter->fileTemplate = parent->fileTemplate;
				if (childIter->directoryTemplate.empty())	childIter->directoryTemplate = parent->directoryTemplate;
				if (childIter->virtualDirectoryTemplate.empty())	childIter->virtualDirectoryTemplate = parent->virtualDirectoryTemplate;
				if (childIter->footerTemplate.empty())	childIter->footerTemplate = parent->footerTemplate;

				// fill default documents list
				default_documents_vector defDocsList = parent->defaultDocuments;

				default_documents_vector::iterator iter = childIter->defaultDocuments.begin(), removeIter;
				while (iter != childIter->defaultDocuments.end()) 
				{
					if (iter->first && std::find (defDocsList.begin(), defDocsList.end(), *iter) == defDocsList.end()) {
						defDocsList.push_back(*iter);

					} else if (!iter->first) {
						removeIter = std::find (defDocsList.begin(), defDocsList.end(), std::make_pair(true, iter->second));
						if (removeIter == defDocsList.end())
							throw settings_load_error ("Cannot remove default document record \"%s\", in directory: %s - "
							"it is not declared in parent directory record.", 
							iter->second.c_str(), 
							childIter->name.c_str());
						else
							defDocsList.erase(removeIter);
					}
					iter++;
				}
				childIter->defaultDocuments = defDocsList;

				// copy plugins registration
				copyHandlersRegistration (*parent, *childIter);

				copyModulesRegistration (*parent, *childIter);

				// store updated directory info
				childIter->number = _directories.size();
				_directories[childIter->virtualPath] = *childIter;

				fillDirectoriesMap (directoriesList, childIter, locaInfoLoadCallback);
			}

		}

	}


	
	void HttpServerSettings::loadGlobalModules (DirectorySettings& root)
	{
		// load global modules
		global_plugins_map::const_iterator moduleIt = RegisteredModules.begin();
		
		for (; moduleIt != RegisteredModules.end(); ++moduleIt) 
		{
			if (moduleIt->second.global) 
			{
				const PluginInfo& info = moduleIt->second;
				
				PluginRegistrationInfo regInfo;

				regInfo.registrationType = PluginRegistration::Add;
				regInfo.pluginName = moduleIt->first;
				regInfo.pluginIndex = info.pluginIndex;

				std::copy (&info.moduleCallbacks[0], 
					&info.moduleCallbacks[0] + ModuleCallbackTypesCount,
					&regInfo.moduleCallbacks[0]);
		
				root.modules.push_back ( regInfo );
			}
		}
	

	}

	void HttpServerSettings::addModuleCallbacksToMap (void** moduleCallbacks, int pluginIndex, callback_map& callbackMap )
	{
		for (int ndx=0; ndx<ModuleCallbackTypesCount; ++ndx)
		{
			if (moduleCallbacks[ndx])
			{
				callback_map::iterator it = callbackMap.find ((ModuleCallbackType) ndx);
				if (it == callbackMap.end()) {
					std::list< std::pair <void*, int> > lst;
					lst.push_back ( std::make_pair (moduleCallbacks[ndx], pluginIndex));

					callbackMap.insert ( std::make_pair ((ModuleCallbackType) ndx, lst)); 
				
				} else {
					it->second.push_back ( std::make_pair (moduleCallbacks[ndx], pluginIndex) );
				}
			}
		
		}
	}


	void HttpServerSettings::fillModulesCallbackInfo ()
	{
		_modulesCallbacks.clear();
	
		directories_map::const_iterator dirIter = _directories.begin();
	
		for (; dirIter != _directories.end(); ++dirIter) 
		{
			callback_map dirCallbacks;

			directory_plugins_list::const_iterator it = dirIter->second.modules.begin();
			for (; it != dirIter->second.modules.end(); ++it) 
			{
				addModuleCallbacksToMap ( (void**) it->moduleCallbacks, it->pluginIndex, dirCallbacks);
			}
			
			_modulesCallbacks[dirIter->second.number] = dirCallbacks;
		}
	}


	void HttpServerSettings::copyHandlersRegistration (const DirectorySettings& parent, DirectorySettings& child)
	{
		directory_plugins_list registeredHandlers = parent.handlers;
		directory_plugins_list::const_iterator handlerIter;

		for (handlerIter = child.handlers.begin(); 
				handlerIter != child.handlers.end(); ++handlerIter) 
		{
			if (handlerIter->registrationType == PluginRegistration::Clear) {
				registeredHandlers.clear();
			
			} else if (handlerIter->registrationType == PluginRegistration::Remove) {
				
				// if no 'ext' attribute defined - remove registration at all
				if (handlerIter->extensions.empty()) {
					registeredHandlers.erase (std::remove_if (registeredHandlers.begin(), 
								registeredHandlers.end(), PluginInfoByNameFinder(handlerIter->pluginName) ), 
							registeredHandlers.end());
				} else {
					directory_plugins_list::iterator it = std::find_if (registeredHandlers.begin(), 
						registeredHandlers.end(), PluginInfoByNameFinder(handlerIter->pluginName) );

					if (it != registeredHandlers.end()) 
					{
					    std::set<string>::const_iterator itExt;
					    
					    for (itExt = handlerIter->extensions.begin(); 
					            itExt != handlerIter->extensions.end(); ++itExt)  {
					        if ( it->extensions.find (*itExt) != it->extensions.end())
					            it->extensions.erase (*itExt);
					    }
				
						it->excludedExtensions.insert (handlerIter->extensions.begin(), handlerIter->extensions.end());
					}
				}
			
			} else if (handlerIter->registrationType == PluginRegistration::Add) {
				
				directory_plugins_list::iterator it = std::find_if (registeredHandlers.begin(), 
						registeredHandlers.end(), PluginInfoByNameFinder(handlerIter->pluginName) );

				if (it == registeredHandlers.end())
					registeredHandlers.push_back(*handlerIter);
				else
					it->extensions.insert (handlerIter->extensions.begin(), handlerIter->extensions.end());
			
			} else if (handlerIter->registrationType == PluginRegistration::Register) {
				// not safe, but will give some freedom od action for user	
				registeredHandlers.push_back(*handlerIter);
			} 
		}

		// apply handlers info intersection
		child.handlers.swap(registeredHandlers);
	}

	void HttpServerSettings::copyModulesRegistration (const DirectorySettings& parent, DirectorySettings& child)
	{
		directory_plugins_list registeredModules = parent.modules;
		
		directory_plugins_list::const_iterator iter;

		for (iter = child.modules.begin(); 
				iter != child.modules.end(); ++iter) 
		{
			if (iter->registrationType == PluginRegistration::Clear) {
				registeredModules.clear();
			
			} else if (iter->registrationType == PluginRegistration::Remove) {
				
				registeredModules.erase (std::remove_if (registeredModules.begin(), 
					registeredModules.end(), PluginInfoByNameFinder(iter->pluginName) ), 
							registeredModules.end());
			
			} else if (iter->registrationType == PluginRegistration::Add
				|| iter->registrationType == PluginRegistration::Register) {
		
				registeredModules.push_back(*iter);
			} 
		}

		// apply modules info intersection
		child.modules.swap(registeredModules);
	}

	
	void HttpServerSettings::loadPluginProperties (string_constref pluginName, 
		PluginInfo& info, 
		PluginType pluginType) throw (settings_load_error)
	{
		global_plugins_map& pluginsMap = (pluginType == PluginHandler ? RegisteredHandlers : RegisteredModules);
		if (pluginsMap.find (pluginName) != pluginsMap.end() )
			throw settings_load_error ("Plugin \"%s\" has been already loaded", pluginName.c_str());

#if defined (WIN32)		
		info.dll = ::LoadLibraryA (info.pathToLoad.c_str());

		if (NULL == info.dll)
			throw settings_load_error ("Plugin loading failed, library: %s, error: %s", 
				info.pathToLoad.c_str(),
				aconnect::util::formatWindowsError(::GetLastError()).c_str());

		if (pluginType == PluginHandler)
		{
			info.processRequestFunc = ::GetProcAddress (info.dll, SettingsConstants::ProcessRequestFunName);
			if (!info.processRequestFunc) 
				throw settings_load_error ("Request processing function loading failed, "
					"library: %s, error code: %d", info.pathToLoad.c_str(), ::GetLastError());
		}
		
		info.initFunc = ::GetProcAddress (info.dll, SettingsConstants::InitFunName);
		if (!info.initFunc) 
			throw settings_load_error ("Plugin initialization function loading failed, "
				"library: %s, error code: %d", info.pathToLoad.c_str(), ::GetLastError());

		info.destroyFunc = ::GetProcAddress (info.dll, SettingsConstants::DestroyFunName);

#else
		info.dll = dlopen (info.pathToLoad.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND );
		if (NULL == info.dll) {
			string_constptr errorMsg = dlerror (); 
			throw settings_load_error ("Plugin loading failed, library: %s, error: %s", 
				info.pathToLoad.c_str(), errorMsg ? errorMsg : "Unknown error");
		}

		if (pluginType == PluginHandler)
		{
			info.processRequestFunc = dlsym (info.dll, SettingsConstants::ProcessRequestFunName);
			if (!info.processRequestFunc) 
				throw settings_load_error ("Request processing function loading failed, "
					"library: %s, error: %s", info.pathToLoad.c_str(), dlerror());
		}

		info.initFunc = dlsym (info.dll, SettingsConstants::InitFunName);
		if (!info.initFunc) 
			throw settings_load_error ("Plugin initialization function loading failed, "
				"library: %s, error: %s", info.pathToLoad.c_str(), dlerror());

		info.destroyFunc = dlsym (info.dll, SettingsConstants::DestroyFunName);
#endif
		if (pluginType == PluginModule)
		{
			info.moduleCallbacks[ModuleCallbackOnRequestBegin] = aconnect::util::getDllSymbol (info.dll, SettingsConstants::OnRequestBeginFunName);
			info.moduleCallbacks[ModuleCallbackOnRequestResolve] = aconnect::util::getDllSymbol (info.dll, SettingsConstants::OnRequestResolveFunName);
			info.moduleCallbacks[ModuleCallbackOnRequestMapHandler] = aconnect::util::getDllSymbol (info.dll, SettingsConstants::OnRequestMapHandlerName);
			
			info.moduleCallbacks[ModuleCallbackOnResponsePreSendHeaders] = aconnect::util::getDllSymbol (info.dll, SettingsConstants::OnResponsePreSendHeadersFunName);
			info.moduleCallbacks[ModuleCallbackOnResponsePreSendContent] = aconnect::util::getDllSymbol (info.dll, SettingsConstants::OnResponsePreSendContentFunName);
			info.moduleCallbacks[ModuleCallbackOnResponseEnd] = aconnect::util::getDllSymbol (info.dll, SettingsConstants::OnResponseEndFunName);
		}

		if (pluginType == PluginHandler)
			RegisteredHandlers[pluginName] = info;
		else if (pluginType == PluginModule)
			RegisteredModules[pluginName] = info;
	}


	void HttpServerSettings::initPlugins (PluginType pluginType, HttpServer* server)
	{
		assert (_logger);
		assert (server);

		global_plugins_map::const_iterator iter;
		global_plugins_map& plugins = (pluginType == PluginHandler ? RegisteredHandlers : RegisteredModules);

		for (iter = plugins.begin(); iter != plugins.end(); ++iter)
		{
			bool inited = reinterpret_cast<init_plugin_function> (iter->second.initFunc) 
				(iter->second.params, iter->second.pluginIndex, server);
			
			if (!inited)
				throw settings_load_error ("Plugin \"%s\" initialization failed failed",
					iter->first.c_str() );
		}
	}

	void HttpServerSettings::destroyPlugins (PluginType pluginType, bool unload)
	{
		global_plugins_map::iterator iter;
		global_plugins_map& plugins = (pluginType == PluginHandler ? RegisteredHandlers : RegisteredModules);

		for (iter = plugins.begin(); 
			iter != plugins.end(); ++iter)
		{
			assert (iter->second.dll && "DLL is already unloaded");
			if (NULL == iter->second.destroyFunc)
				continue;
			
			reinterpret_cast<destroy_plugin_function> (iter->second.destroyFunc) ();

			if (unload) {
				aconnect::util::unloadLibrary (iter->second.dll);
				iter->second.dll = NULL;
			}
		}
	}

	//==================================================================//
	//	Environment preparation											//
	//==================================================================//
	
	void HttpServerSettings::applyLocale (string_constref localeName)
	{
		char* res = setlocale (LC_CTYPE, localeName.c_str());
		if (NULL == res)
			throw settings_load_error ("Unsupported locale defined: %s", localeName.c_str());
	}

	void HttpServerSettings::setupGlobalUploadsDir ()
	{
		if (!GlobalUploadsDirectory.empty()) 
		{
			updateAppLocationInPath (GlobalUploadsDirectory);

			fs::path uploadsDir (GlobalUploadsDirectory, fs::native); 

			if (!fs::exists (uploadsDir)) {
				try {
					fs::create_directories(uploadsDir);

				} catch (std::runtime_error& err) {
					throw settings_load_error("Upload directory creation failed, exception: %s, message: %s", 
						typeid(err).name(), err.what());
				}
			}
		}
		

	}

	//==================================================================//
	//		Helpers														//
	//==================================================================//

	std::set<string> HttpServerSettings::parseExtensions(string_constref ext) 
	{

		std::set<string> res;
		aconnect::SimpleTokenizer tokens(ext, ";");
		
		for (aconnect::SimpleTokenizer::iterator tok_iter = tokens.begin(); 
				tok_iter != tokens.end(); 
				++tok_iter)
		{
			string token = tok_iter.current_token();
			algo::trim (token);

			if (token.empty())
				continue;

			res.insert (token);
		}

			
		return res;
	}

	void HttpServerSettings::updateAppLocationInPath (aconnect::string &pathStr) const 
	{
		if ( pathStr.find(SettingsConstants::AppPathMark) != aconnect::string::npos)
			algo::replace_first (pathStr, SettingsConstants::AppPathMark, AppLocation);
	}


} // namespace ahttp

